/******************************************************************************

    Copyright (C) 2006 - 2011 Alessio Stalla (alessiostalla@gmail.com)

    This file is part of PrettyProlog.

    PrettyProlog is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    PrettyProlog is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with PrettyProlog; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    See the file COPYING for further details.

 *****************************************************************************/

package prettyprolog.gui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.util.Iterator;
import java.util.Vector;

import javax.swing.AbstractListModel;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListCellRenderer;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;

import prettyprolog.datatypes.Namespace;
import prettyprolog.datatypes.Variable;
import prettyprolog.engine.Clause;
import prettyprolog.engine.Domain;
import prettyprolog.engine.NotAClauseException;
import prettyprolog.engine.Theory;
import prettyprolog.engine.listeners.TheoryEvent;
import prettyprolog.engine.listeners.TheoryListener;
import prettyprolog.parser.ParseException;
import prettyprolog.parser.Parser;

/** A panel showing a Theory and some buttons for modifying it. */
public class TheoryViewer extends PrettyPrologPanel {

	private static final long serialVersionUID = 1L;
	private static String newline = System.getProperty("line.separator", "\n");
	private TheoryViewer self = this;
	private Theory theory;
	private JList theory_list = new JList();
	private JButton assert_b = new JButton("assert");
	private JButton asserta_b = new JButton("asserta");
	private JButton retract_b = new JButton("retract");

	private JTextField clause = new JTextField(25);
	private JButton up_b;
	private JButton down_b;
	private Parser parser;

	/** The last opened file. */
	private File last_file = null;

	private TheoryListener theory_listener;

	// Menu Items
	private JMenuItem load_item = new JMenuItem("Load theory");
	private JMenuItem save_item = new JMenuItem("Save theory");
	private JMenuItem clear_item = new JMenuItem("Clear theory");
	private JSeparator sep = new JSeparator(SwingConstants.HORIZONTAL);

	/** Creates a new TheoryViewer. */
	public TheoryViewer(Parser parser) {
		super("Theory Viewer");
		this.parser = parser;
		setLayout(new BorderLayout());
		add(new JScrollPane(theory_list));
		JPanel theory_bottom_p = new JPanel(new BorderLayout());
		JPanel tmp_panel = new JPanel(new FlowLayout());
		tmp_panel.add(new JLabel("Clause: "));
		tmp_panel.add(clause);
		theory_bottom_p.add(tmp_panel);
		tmp_panel = new JPanel(new FlowLayout());
		assert_b.setToolTipText("Add the clause at the end of the theory.");
		tmp_panel.add(assert_b);
		asserta_b
				.setToolTipText("Add the clause at the beginning of the theory.");
		tmp_panel.add(asserta_b);
		retract_b.setToolTipText("Remove the clause(s) from the theory.");
		tmp_panel.add(retract_b);

		theory_bottom_p.add(tmp_panel, BorderLayout.SOUTH);
		add(new JScrollPane(theory_bottom_p), BorderLayout.SOUTH);
		assert_b.setEnabled(false);
		asserta_b.setEnabled(false);
		retract_b.setEnabled(false);

		tmp_panel = new JPanel();
		tmp_panel.setLayout(new BoxLayout(tmp_panel, BoxLayout.Y_AXIS));
		tmp_panel.add(Box.createVerticalGlue());
		ImageIcon icon = null;
		java.net.URL url = getClass().getResource("/prettyprolog.gui.images/up.png");
		if (url != null) {
			// Image image = Toolkit.getDefaultToolkit().getImage(url);
			icon = new ImageIcon(url);
		}
		if (icon != null)
			up_b = new JButton(icon);
		else
			up_b = new JButton("Up");
		up_b.setAlignmentX(0.5f);
		up_b.setMaximumSize(new Dimension(Short.MAX_VALUE, up_b
				.getMaximumSize().height));
		up_b.setEnabled(false);
		tmp_panel.add(up_b);
		tmp_panel.add(Box.createRigidArea(new Dimension(0, 5)));
		ImageIcon icon2 = null;
		url = getClass().getResource("/prettyprolog.gui.images/down.png");
		if (url != null) {
			//Image image = Toolkit.getDefaultToolkit().getImage(url);
			icon2 = new ImageIcon(url);
		}
		if (icon2 != null)
			down_b = new JButton(icon2);
		else
			down_b = new JButton("Down");
		down_b.setAlignmentX(0.5f);
		down_b.setMaximumSize(new Dimension(Short.MAX_VALUE, down_b
				.getMaximumSize().height));
		down_b.setEnabled(false);
		tmp_panel.add(down_b);
		tmp_panel.add(Box.createVerticalGlue());
		add(tmp_panel, BorderLayout.EAST);
		setupListeners();
		setupMenus();
	}

	public void setOwner(PrettyPrologApplet o) {
		super.setOwner(o);
		JMenu file_menu = owner.getJMenuBar().getMenu(PrettyPrologApplet.FILE_MENU);
		file_menu.add(load_item, 0);
		file_menu.add(save_item, 1);
		file_menu.add(clear_item, 2);
		if (file_menu.getItemCount() > 3)
			file_menu.add(sep, 3);
		theory = owner.getEngine().getTheory();
		if (theory == null) {
			assert_b.setEnabled(false);
			asserta_b.setEnabled(false);

		} else {
			theory.addTheoryListener(theory_listener);
			showTheory();
		}
	}

	public void removeFromOwner() {
		if (owner != null) {
			theory.removeTheoryListener(theory_listener);
			JMenu file_menu = owner.getJMenuBar().getMenu(PrettyPrologApplet.FILE_MENU);
			file_menu.remove(load_item);
			file_menu.remove(save_item);
			file_menu.remove(clear_item);
			file_menu.remove(sep);
		}
		super.removeFromOwner();
		theory = new Theory(null); //TODO?
		showTheory();
	}

	@SuppressWarnings("serial")
	private void setupListeners() {

		// Enable the "assert" button only when the user writes something
		// in the "clause" text box.
		clause.setDocument(new PlainDocument() {

			public void insertString(int offs, String str, AttributeSet a)
					throws BadLocationException {
				super.insertString(offs, str, a);
				if ((getLength() > 0) && (theory != null)) {
					assert_b.setEnabled(true);
					asserta_b.setEnabled(true);

				}
			}

			public void remove(int offs, int len) throws BadLocationException {
				super.remove(offs, len);
				if (getLength() == 0) {
					assert_b.setEnabled(false);
					asserta_b.setEnabled(false);
				}
			}
		});

		theory_listener = new TheoryListener() {

			public void clauseAsserted(TheoryEvent e) {
				showTheory();
			}

			public void clauseRetracted(TheoryEvent e) {
				showTheory();
			}

			public void clauseSelected(TheoryEvent e) {
				theory_list.setSelectedIndex(e.getIndex());
				theory_list.ensureIndexIsVisible(e.getIndex());
			}
		};

		// Enables some buttons only when the user has selected
		// at least a clause in the theory.
		theory_list.addListSelectionListener(new ListSelectionListener() {
			public void valueChanged(ListSelectionEvent e) {
				boolean sel = theory_list.getSelectedValue() != null;
				retract_b.setEnabled(sel);
				up_b.setEnabled(sel);
				down_b.setEnabled(sel);
			}
		});

		ActionListener assert_l = new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				if (!assert_b.isEnabled()) {
					return;
				}
				try {
				    //TODO ...
				    Namespace.setCurrent(owner.getEngine().getNamespace());
					theory.assertz((Clause) parser.parse(parser.Clause(), clause.getText()));
				} catch (Exception _e) {
					_e.printStackTrace();
					JOptionPane.showMessageDialog(self, clause.getText()
							+ " is not a valid clause" + newline + "(" + _e
							+ ")", "Error", JOptionPane.ERROR_MESSAGE);
				}
			}
		};

		assert_b.addActionListener(assert_l);
		clause.addActionListener(assert_l);

		asserta_b.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				try {
                    //TODO ...
				    Namespace.setCurrent(owner.getEngine().getNamespace());
					theory.asserta((Clause) parser.parse(parser.Clause(), clause.getText()));
				} catch (Exception _e) {
					JOptionPane.showMessageDialog(self, clause.getText()
							+ " is not a valid clause" + newline + "(" + _e
							+ ")", "Error", JOptionPane.ERROR_MESSAGE);
				}
			}
		});
		retract_b.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				Object[] values = theory_list.getSelectedValues();
				for (int i = 0; i < values.length; i++)
					theory.retract((Clause) values[i]);
			}
		});
			
   

		up_b.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				Object[] values = theory_list.getSelectedValues();
				int[] indices = theory_list.getSelectedIndices();
				for (int i = 0; i < values.length; i++) {
					indices[i]--;
					if (indices[i] < 0) {
						indices[i] = 0;
						break;
					}
					theory.retract((Clause) values[i]);
					theory.assertClause((Clause) values[i], indices[i]);
				}
				theory_list.setSelectedIndices(indices);
			}
		});

		down_b.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				Object[] values = theory_list.getSelectedValues();
				int[] indices = theory_list.getSelectedIndices();
				int sz = theory.size();
				for (int i = values.length - 1; i >= 0; i--) {
					indices[i]++;
					if (indices[i] > sz - 1) {
						indices[i] = sz - 1;
						break;
					}
					theory.retract((Clause) values[i]);
					theory.assertClause((Clause) values[i], indices[i]);
				}
				theory_list.setSelectedIndices(indices);
			}
		});

		load_item.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				loadTheory();
			}
		});

		save_item.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				saveTheory();
			}
		});

		clear_item.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				owner.getEngine().resetTheory();
			}

		});

	}

	/**
	 * Loads a Theory from a character stream. If not running as an applet, the
	 * stream reads from a file; else, an input dialog is shown where the user
	 * can type in the theory.
	 */
	private void loadTheory() {
	    Namespace.setCurrent(owner.getEngine().getNamespace());
		if (!owner.isApplet()) {
			loadTheoryFromFile();
		} else
			loadTheoryFromDialog();
	}

	/**
	 * If running as an applet, shows a non-editable text area from which the
	 * user can copy the theory. Else, it saves the theory on a file, in plain
	 * text format. (Subsequent versions might offer the option save the Theory
	 * as xml or S-expressions as well)
	 */
	private void saveTheory() {
		if (!owner.isApplet()) {
			saveTheoryToFile();
		} else {
			saveTheoryToDialog();
		}
	}

	private void loadTheoryFromFile() {
		JFileChooser ch = new JFileChooser();
		if (last_file != null)
			ch.setSelectedFile(last_file);
		Theory old_theory = null; //FIXME old_theory looks wrong (double assert on fail?)
		if (ch.showOpenDialog(owner) == JFileChooser.APPROVE_OPTION)
			try {
				BufferedReader buf = new BufferedReader(new FileReader(
						ch.getSelectedFile()));
				old_theory = theory.copy();
				owner.getEngine().loadTheory(buf);
				buf.close();
				last_file = ch.getSelectedFile();
			} catch (IOException _e) {
				String msg = "Error: can't open " + newline
						+ ch.getSelectedFile() + newline + "(" + _e + ")";
				JOptionPane.showMessageDialog(owner, msg, "Error",
						JOptionPane.ERROR_MESSAGE);
			} catch (ParseException _e) {
				String msg = "Error: can't parse " + newline
						+ ch.getSelectedFile() + newline + "(" + _e + ")";
				JOptionPane.showMessageDialog(owner, msg, "Error",
						JOptionPane.ERROR_MESSAGE);
				owner.getEngine().resetTheory();
				owner.getEngine().loadTheory(old_theory);
			} catch (NotAClauseException _e) {
				String msg = "Error: " + _e.getTerm() + " is not a clause";
				JOptionPane.showMessageDialog(owner, msg, "Error",
						JOptionPane.ERROR_MESSAGE);
				owner.getEngine().resetTheory();
				owner.getEngine().loadTheory(old_theory);
			}
	}

	private void saveTheoryToFile() {
		JFileChooser ch = new JFileChooser();
		if (last_file != null)
			ch.setSelectedFile(last_file);
		if (ch.showSaveDialog(owner) == JFileChooser.APPROVE_OPTION)
			try {
				BufferedWriter buf = new BufferedWriter(new FileWriter(
						ch.getSelectedFile()));
				for (Iterator<Clause> it = theory.iterator(); it.hasNext();)
					buf.write(it.next().toString() + newline);
				buf.close();
				last_file = ch.getSelectedFile();
			} catch (IOException _e) {
				String msg = "Error: can't open " + newline
						+ ch.getSelectedFile() + newline + "(" + _e + ")";
				JOptionPane.showMessageDialog(owner, msg, "Error",
						JOptionPane.ERROR_MESSAGE);
			}
	}

	@SuppressWarnings("serial")
	private class TheoryDialog extends JDialog {

		private JTextArea input = new JTextArea();
		private boolean ok = false;

		public TheoryDialog(boolean allow_edit, String label) {
			super((java.awt.Frame) null, "Theory", true);
			setLayout(new BorderLayout());
			add(new JLabel(label), BorderLayout.NORTH);
			input.setEditable(allow_edit);
			add(new JScrollPane(input), BorderLayout.CENTER);
			JButton ok_btn = new JButton("Ok");
			ok_btn.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					ok = true;
					dispose();
				}
			});
			JButton canc_btn = new JButton("Cancel");
			canc_btn.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					ok = false;
					dispose();
				}
			});
			addWindowListener(new WindowAdapter() {
				public void windowClosing(WindowEvent e) {
					ok = false;
					dispose();
				}
			});
			JPanel tmp_panel = new JPanel(new FlowLayout());
			tmp_panel.add(ok_btn);
			if (allow_edit)
				tmp_panel.add(canc_btn);
			add(tmp_panel, BorderLayout.SOUTH);
			setSize(450, 300);
		}

		public String getText() {
			setVisible(true);
			if (ok)
				return input.getText();
			else
				return null;
		}

		public void setText(String s) {
			input.setText(s);
		}

	};

	private void loadTheoryFromDialog() {
		TheoryDialog dlg = new TheoryDialog(true,
				"Type in your Theory, or use cut'n'paste:");
		String input = dlg.getText();
		if (input == null)
			return;
		Theory old_theory = null;
		try {
			BufferedReader buf = new BufferedReader(new StringReader(input));
			old_theory = theory.copy();
			owner.getEngine().loadTheory(buf);
			buf.close();
		} catch (IOException _e) {
			String msg = "There was an i/o exception " + newline + "(" + _e
					+ ")";
			JOptionPane.showMessageDialog(owner, msg, "Error",
					JOptionPane.ERROR_MESSAGE);
			owner.getEngine().resetTheory();
			owner.getEngine().loadTheory(old_theory);
		} catch (ParseException _e) {
			String msg = "Error: can't parse the Theory " + newline + "(" + _e
					+ ")";
			JOptionPane.showMessageDialog(owner, msg, "Error",
					JOptionPane.ERROR_MESSAGE);
			owner.getEngine().resetTheory();
			owner.getEngine().loadTheory(old_theory);
		} catch (NotAClauseException _e) {
			String msg = "Error: " + _e.getTerm() + " is not a clause";
			JOptionPane.showMessageDialog(owner, msg, "Error",
					JOptionPane.ERROR_MESSAGE);
			owner.getEngine().resetTheory();
			owner.getEngine().loadTheory(old_theory);
		}
	}

	private void saveTheoryToDialog() {
		TheoryDialog dlg = new TheoryDialog(false,
				"Copy the Theory from below, then paste it into your favorite text editor:");
		StringBuffer buf = new StringBuffer();
		for (Iterator<Clause> it = theory.iterator(); it.hasNext();)
			buf.append(it.next().toString() + newline);
		dlg.setText(buf.toString());
		dlg.setVisible(true);
	}

	private void setupMenus() {

		load_item.addChangeListener(new ChangeListener() {

			public void stateChanged(ChangeEvent e) {
				if (load_item.isArmed())
					owner.showStatus("Loads a Theory and appends it to the current one.");
				else
					owner.showStatus("");
			}

		});

		save_item.addChangeListener(new ChangeListener() {

			public void stateChanged(ChangeEvent e) {
				if (save_item.isArmed())
					owner.showStatus("Saves the current Theory.");
				else
					owner.showStatus("");
			}

		});

		clear_item.addChangeListener(new ChangeListener() {

			public void stateChanged(ChangeEvent e) {
				if (clear_item.isArmed())
					owner.showStatus("Removes all clauses from the current Theory.");
				else
					owner.showStatus("");
			}

		});

	}

	// Updates the theory list.
	@SuppressWarnings("serial")
	private void showTheory() {
		theory_list.setModel(new AbstractListModel() {

			public Object getElementAt(int i) {
				return theory.getClause(i);
			}

			public int getSize() {
				return theory.size();
			}
		});
		theory_list.setCellRenderer(new DefaultListCellRenderer() {
		    @Override
		    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
		        Namespace old = Namespace.current();
		        Namespace.setCurrent(owner.getEngine().getNamespace());
		        try {
		            return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
		        } finally {
		            Namespace.setCurrent(old);
		        }
		    }
		});
	}
}